package nl.Viewer.Sessions;

import java.util.ArrayList;

import nl.Viewer.Application;
import nl.Viewer.SerializedObjects.DataObject;
import nl.Viewer.SerializedObjects.Job;
import nl.Viewer.SerializedObjects.Parameter;
import nl.Viewer.SerializedObjects.ParameterValue;
import nl.Viewer.SerializedObjects.Session;
import nl.Viewer.Sessions.Communication.SOAPServerException;
import nl.Viewer.Sessions.Communication.UserRequestException;
import nl.Viewer.Sessions.Communication.XMLParseException;
import nl.Viewer.ViewManager.Controllers.GenericController;

/**
 * This is a pivotal class maintaining the state of a session on the client side. This class has a thread running that
 * will - submit all (view-)requested jobs - check for progress - download all data parts that will become available on
 * the server - there is initial support for creating download tasks, for example for Zoomify data objects: currently
 * all data parts will be fetched, starting at the lowest resolution, row by row, from left to right
 * 
 * @author Jeroen Verschuur
 * 
 */

public class ClientSideSession extends Thread {
   public final long               sessionId;
   private volatile boolean        running           = false;
   private SessionManager          parent;
   private RenderJob               waitingJob        = null;
   private RenderJob               runningJob        = null;

   protected volatile long         lastFinishedJobId = -1;

   // the output type the ViewManager agreed on
   private String                  outputType        = null;

   // download tasks
   private ArrayList<DownloadTask> tasks             = new ArrayList<DownloadTask>();
   protected boolean               shuttingDown      = false;

   public ClientSideSession( long sessionId, SessionManager parent ) {
      this.sessionId = sessionId;
      this.parent = parent;

      start();
   }

   public String toString() {
      return "Session: " + sessionId;
   }

   public void shutdown() {
      clearTasks();
      shuttingDown = true;
      running = false;
   }

   /**
    * Update job(statuses) for a list of server sessions
    * 
    * @param s
    */

   public synchronized void updateSession( Session s ) {
      // System.out.println( "Update received for " + this + ": " + s );

      GenericController view = Application.viewManager.getViewController( sessionId );
      if( view == null ) {
         // probably already closed on client side
         System.out.println( "View not found for session: " + sessionId );
      } else {
         view.updateProgress( s.algorithm, s.progress );
      }

      if( s.status == Session.Status.CLOSED || s.status == Session.Status.FAILED ) {
         String message = "The session has been closed by the server";
         if( s.reason != null && s.reason.length() > 0 ) {
            message += " (reason: " + s.reason + ")";
         }
         // Application.showErrorMessage( "Session closed", message );
         // TODO: close the window?
      }

      RenderJob job = getRunningJob();
      if( job == null ) {
         // no job running
         return;
      }

      for( Job j : s.alteredJobs ) {
         if( j.jobId == job.jobId ) {
            job.status = j.getStatus();
         }
      }
   }

   private synchronized void clearTasks() {
      tasks.clear();
   }

   private synchronized RenderJob getWaitingJob() {
      return waitingJob;
   }

   private synchronized RenderJob getRunningJob() {
      return runningJob;
   }

   private synchronized DownloadTask getNextTask() {
      if( tasks.size() > 0 ) {
         return tasks.remove( 0 );
      }
      return null;
   }

   private synchronized void setRunningJob( RenderJob job ) {
      runningJob = job;
   }

   private synchronized void setWaitingJob( RenderJob job ) {
      waitingJob = job;
   }

   private void createDownloadTasks( long jobId, DataObject dataObject ) {
      // start at lowest resolution, line by line
      for( int r = 0; r < dataObject.getResolutionLevelCount(); r++ ) {
         for( int y = 0; y < dataObject.getResolutionParts()[r].height; y++ ) {
            for( int x = 0; x < dataObject.getResolutionParts()[r].width; x++ ) {
               tasks.add( new DownloadTask( jobId, r, x, y ) );
            }
         }
      }
   }

   public void run() {
      RenderJob job;
      DownloadTask task;

      running = true;
      while( running ) {
         if( ( job = getWaitingJob() ) != null ) { // first check to see if there is a job waiting to be submitted
            // clear all download tasks
            clearTasks();

            // submit new job
            setWaitingJob( null );
            try {
               setRunningJob( job );
               submitJob( job );
            }
            catch( Exception e ) {
               setRunningJob( null );
               e.printStackTrace();
            }

         } else if( ( job = getRunningJob() ) != null && ( job.status == Job.Status.FINISHED || job.status == Job.Status.FAILED ) ) {
            // check if we have submitted a job and that job is either finished or failed

            if( job.status == Job.Status.FINISHED ) {

               // store this id, which will be used when setting a user-defined lookmark from the user-
               // interface
               lastFinishedJobId = job.jobId;

               try {

                  GenericController view = Application.viewManager.getViewController( sessionId );
                  if( view == null ) {
                     System.out.println( "View not found for session: " + sessionId );
                     shutdown();
                  } else {
                     // get all job related information from the server
                     ArrayList<ParameterValue> parameterValues = parent.getCommunication().getJobParameters( job.jobId );

                     DataObject dataObject = null;
                     for( int i = 0; i < parameterValues.size(); i++ ) {
                        ParameterValue pv = parameterValues.get( i );
                        if( pv.parameterName.equals( "DataObject" ) ) {
                           dataObject = (DataObject)pv.value;
                        }
                     }
                     // prepare a new datastore
                     view.clearDataStore();

                     if( dataObject == null ) {
                        System.out.println( "Warning: returned job parameters did not contain a dataobject description" );
                     } else {
                        // create a new datastore with the correct data type
                        view.createNewDataStore( dataObject );

                        // update the parameters for the view
                        view.updateParameterValues( parameterValues, false );

                        // createDownloadTasks
                        createDownloadTasks( job.jobId, dataObject );

                     }
                  }

               }
               catch( Exception e ) {
                  System.out.println( "Fetching job failed: " + e );
                  e.printStackTrace();
               }
            } else {
               System.out.println( "Job failed: " + job.jobId );
            }

            // clear the running job
            setRunningJob( null );

         } else if( ( task = getNextTask() ) != null ) { // check if we have download work available

            try {
               GenericController view = Application.viewManager.getViewController( sessionId );
               if( view == null ) {
                  System.out.println( "View not found for session: " + sessionId );
                  shutdown();
               } else {

                  // System.out.println( "GetDataPart by " + Thread.currentThread().hashCode() +",
                  // "+parent.getCommunication().hashCode()+", "+parent.getCommunication().service );

                  ArrayList<?> aps = parent.getCommunication().getDataPartAttached( task.jobId, task.resolution,
                        task.xPos, task.yPos );

                  if( aps.size() > 1 ) {
                     view.updateData( (byte[])aps.get( 1 ), task.resolution, task.xPos, task.yPos );
                  }
                  aps.clear();
                  aps = null;
               }
            }
            catch( Exception e ) {
               System.out.println( "Task failed: " + task.jobId ); 
               e.printStackTrace();
            }

         } else { // otherwise: nothing to do, idle around:
            try {
               Thread.sleep( 1 );
            }
            catch( InterruptedException ie ) {
               // ignore
            }
         }
      }

      System.out.println( "Client session thread stopped" );
      if( !shuttingDown ) {
         parent.closeSession( this );
      }

   }

   public synchronized void requestRender( ArrayList<Parameter> changedParameters ) {
      // TODO: if we get a snapshot of the Parameter objects, their actual values may be altered after the snapshot
      // anyway! Basically, we should get the two arrays that are constructed below as the snapshot, instead of the list
      // of Parameter objects

      String[] parameterNames = new String[ changedParameters.size() ];
      Object[] parameterValues = new Object[ changedParameters.size() ];

      for( int i = 0; i < changedParameters.size(); i++ ) {
         parameterNames[i] = changedParameters.get( i ).getParameterName();
         parameterValues[i] = changedParameters.get( i ).getParameterValue();
      }
      RenderJob job = new RenderJob( parameterNames, parameterValues );
      setWaitingJob( job );
   }

   public void getSessionParametersFromServer( ClientSideSession session, String outputType,
         GenericController targetView ) throws SOAPServerException, XMLParseException, UserRequestException {

      ArrayList<Parameter> params = parent.getCommunication().getParametersForSessionWithOutput( session.sessionId,
            outputType );

      targetView.setParameters( params );

      this.outputType = outputType;
   }

   private long submitJob( RenderJob job ) throws SOAPServerException, UserRequestException {
      long jobId = parent.getCommunication().submitJob( sessionId, outputType, job.parameterNames, job.parameterValues );

      job.jobId = jobId;
      return jobId;
   }

   public ArrayList<ParameterValue> getJobParameters( long jobId ) throws SOAPServerException, XMLParseException,
         UserRequestException {
      return parent.getCommunication().getJobParameters( jobId );
   }

   public void createUserDefinedLookmark( boolean isPersonal, String description ) throws SOAPServerException,
         UserRequestException {

      if( lastFinishedJobId < 0 )
         throw new UserRequestException( "Session waiting for initial render" );

      System.out.println( this.hashCode() + ": Creating user defined lookmark for job id " + lastFinishedJobId );
      parent.getCommunication().createUserDefinedLookmark( lastFinishedJobId, isPersonal, description );

   }

   public long getLastFinishedJobId() {
      return lastFinishedJobId;
   }

}

/**
 * A wrapper object for requesting a job from the server
 * 
 * @author Administrator
 * 
 */

class RenderJob {
   protected String[]   parameterNames;
   protected Object[]   parameterValues;
   protected long       jobId  = -1;
   protected Job.Status status = Job.Status.WAITING;

   public RenderJob( String[] parameterNames, Object[] parameterValues ) {
      if( parameterNames == null )
         this.parameterNames = new String[ 0 ];
      else
         this.parameterNames = parameterNames;

      if( parameterValues == null )
         this.parameterValues = new Object[ 0 ];
      else
         this.parameterValues = parameterValues;

   }
}

/**
 * A wrapper for a task to download a specific data part
 * 
 * @author Jeroen Verschuur
 * 
 */

class DownloadTask {
   public final long jobId;
   public final int  resolution;
   public final int  xPos;
   public final int  yPos;

   public DownloadTask( long jobId, int resolution, int xPos, int yPos ) {
      this.jobId = jobId;
      this.resolution = resolution;
      this.xPos = xPos;
      this.yPos = yPos;
   }

}
